<!DOCTYPE html>
<html lang="en">
<head>
<title>WLED File Editor</title>
<meta name="author" content="DedeHai, based on editor by Me-No-Dev">
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport"><!-- prevent too much scaling on mobile -->
<link rel="shortcut icon" href="favicon.ico">
<link href="style.css" rel="stylesheet">
<!-- Optional lightweight JSON editor - fallback to textarea if CDN is unavailable -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.23.4/ace.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.23.4/mode-json.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.23.4/theme-monokai.min.js"></script>
<script src="common.js"></script>
<style>
/* Editor-specific styles */
body {
	display: flex;
	flex-direction: column;
	min-height: 100vh;
	margin: 0;
	min-width: 850px; /* prevent layout breakdown on small screens */
}
#top {
	font-size: 20px;
	font-weight: bold;
	display: flex;
	align-items: center;
	gap: 8px;
	background: #222;
	padding: 10px 10px;
}
#top .center {
	flex: 1;
	text-align: center;
}
#top .right {
	margin-left: auto;
}
#top input[type="text"] {
	background: #555;
	border: 2px solid #555;
	border-radius: 8px;
	padding: 6px 8px;
	min-width: 200px;
}
#tree {
	position: absolute;
	top: 0;
	bottom: 0;
	left: 0;
	width: 300px;
	background: #222;
	overflow-y: auto;
	padding: 8px 8px 100px;  /* extra space on bottom so context menu always fits */
	text-align: left;
}
#editor, #preview {
	position: absolute;
	top: 0;
	bottom: 0;
	left: 300px;
	right: 0;
	background: #333;
}
#editor {
	display: flex;
	flex-direction: column;
}
#editor textarea {
	flex: 1;
	background: #333;
	color: #fff;
	border: 2px solid #333;
	padding: 8px;
	font: 13px monospace;
	resize: none;
	outline: none;
}
#ace-editor {
	flex: 1;
}
#preview {
	display: none;
	padding: 10px;
	text-align: center;
}
#preview img {
	image-rendering: pixelated;
	width: 40%;
	height: auto;
}
#loader {
	position: fixed;
	top: 50%;
	left: 50%;
	transform: translate(-50%, -50%);
	display: none;
}
.loader {
	width: 60px;
	height: 60px;
	border: 6px solid #444;
	border-top-color: #28f;
	border-radius: 50%;
	animation: spin 1s linear infinite;
}
/* Ace editor colors to match WLED style*/
.ace-monokai .ace_string { color: #4c4 !important; }
.ace-monokai .ace_constant.ace_numeric { color: #fa0 !important; }
.ace-monokai .ace_constant.ace_language { color: #f84 !important; }
.ace-monokai .ace_variable { color: #28f !important; }
.ace_editor { font: 13px monospace !important; }
@keyframes spin { to { transform: rotate(360deg); } }
</style>
<script>
var QueuedRequester = function(){ this.q=[]; this.r=false; this.x=null; }
QueuedRequester.prototype = {
	_request: function(req){
		this.r = true;
		var that = this;
		function cb(x,d){ return function(){
			if (x.readyState==4){
				gId("loader").style.display="none";
				d.callback(x.status,x.responseText);
				if (that.q.length===0) that.r=false;
				if (that.r) that._request(that.q.shift());
			}
		}}
		gId("loader").style.display="block";
		var p="";
		if (req.params instanceof FormData) p=req.params;
		else if (req.params instanceof Object){
			for(var key in req.params){
				p+=(p===""?(req.method==="GET"?"?":""):"&")+encodeURIComponent(key)+"="+encodeURIComponent(req.params[key]);
			}
		}
		this.x=new XMLHttpRequest();
		this.x.onreadystatechange=cb(this.x,req);
		if (req.method==="GET"){ 
			this.x.open(req.method, req.url+p, true);
			this.x.send();
		}
		else{ 
			this.x.open(req.method, req.url, true);
			if (typeof p === "string") this.x.setRequestHeader("Content-type","application/x-www-form-urlencoded");
			this.x.send(p);
		}
	},
	add: function(method,url,params,cb){
		this.q.push({url:url,method:method,params:params,callback:cb});
		if (!this.r) this._request(this.q.shift());
	}
};
var req=new QueuedRequester();
var globalTree; // Single global reference for tree refresh

function loadPreview(filename, editor) {
	var pathField = gId("filepath");
	pathField.value = filename;
	if (/\.(png|jpg|jpeg|gif|bmp|webp)$/i.test(filename)) {
		gId("editor").style.display="none";
		gId("preview").style.display="block";
		gId("preview").innerHTML = '<img src="/edit?func=edit&path=' + encodeURIComponent(filename) + '&_cb=' + Date.now() + '">';
	} else {
		editor.loadText(filename);
	}
}

function refreshTree() {
	if (globalTree) globalTree.refreshPath("/");
}

function createTop(element, editor){
	var input = cE("input");
	input.type = "file";
	input.style.display = "none"; // Hide the default file input

	// Create container structure
	var leftDiv = cE("div");
	leftDiv.className = "left";
	var centerDiv = cE("div");
	centerDiv.className = "center";
	var rightDiv = cE("div");
	rightDiv.className = "right";

	// Single text field for filename
	var path = cE("input");
	path.id = "filepath";
	path.type = "text";
	path.maxLength = "31"; // limit filename length

	var uploadBtn = cE("button"); uploadBtn.className = "sml"; uploadBtn.innerHTML = "Upload File";
	var clearBtn = cE("button"); clearBtn.className = "sml"; clearBtn.innerHTML = "Clear";
	var saveBtn = cE("button"); saveBtn.className = "sml"; saveBtn.innerHTML = "Save";
	var backBtn = cE("button"); backBtn.className = "sml"; backBtn.innerHTML = "Back to the controls"; backBtn.onclick = function(){ window.location.href = getURL("/"); };

	// Add elements to Top
	leftDiv.appendChild(path);
	leftDiv.appendChild(clearBtn);
	leftDiv.appendChild(saveBtn);
	leftDiv.appendChild(uploadBtn);
	centerDiv.innerHTML = "WLED File Editor";
	rightDiv.appendChild(backBtn);
	gId(element).appendChild(input);
	gId(element).appendChild(leftDiv);
	gId(element).appendChild(centerDiv);
	gId(element).appendChild(rightDiv);

	editor.clearEditor();

	uploadBtn.onclick = function() { input.click(); }; // invokes file selector

	function httpPostCb(st,resp){
		if (st!=200) alert("ERROR "+st+": "+resp);
		else {
			showToast("Upload successful!");
			refreshTree();
		}
	}

	// Clear button - clears the editor
	clearBtn.onclick = function(){
		editor.clearEditor(); // Clear editor, file will be created on save
		input.value = ""; // Clear the file selection
	};

	saveBtn.onclick = function(){ editor.save(); };

	// Handle file selection and upload
	input.onchange = function(){
		if (input.files.length == 0) return;
		var file = input.files[0];
		var fd = new FormData();
		fd.append("file", file, file.name);
		console.log("Uploading:", file.name);
		req.add("POST", "/upload", fd, function(st, resp) {
			httpPostCb(st, resp);
			if(st == 200) { loadPreview(file.name, editor); }
		});
		input.value = ""; // Clear the file selection
	};
}

function createTree(element, editor){
	var treeRoot=cE("div");
	gId(element).appendChild(treeRoot);
	var menuCleanup = null; // Track menu cleanup function

	function downloadFile(p){
		window.open(getURL("/edit") + "?func=download&path=" + encodeURIComponent(p), "_blank");
	}

	function deleteFile(p) {
		if (!confirm("Delete " + p + "?")) return;
		req.add("GET", getURL("/edit"), { func:"delete", path:p }, function(st, resp) {
			if (st != 200) alert("ERROR " + st + ": " + resp);
			else refreshTree();
		});
	}

	function createLeaf(path,name,size){
		var leaf=cE("div");
		leaf.style.cssText="cursor:pointer;padding:2px 4px;border-radius:2px;position:relative";
		leaf.textContent=name;
		var span = cE("span");
		span.style.cssText = "font-size: 14px; color: #aaa; margin-left: 8px;";
		span.textContent =  (size > 0 ? Math.max(0.1, (size / 1024)).toFixed(1) : 0) + "KB"; // show size in KB, minimum 0.1 to not show 0KB for small files
		leaf.appendChild(span);
		leaf.onmouseover=function(){ leaf.style.background="#333"; };
		leaf.onmouseout=function(){ leaf.style.background=""; };
		leaf.onclick=function(){ loadPreview(name, editor); };

		// Right-click context menu
		leaf.oncontextmenu = function(e) {
			e.preventDefault();
			// Clean up previous menu
			if (menuCleanup) menuCleanup();
			var menu = cE("div");
			menu.id = "context-menu";
			menu.style.cssText = "position:fixed;left:"+e.clientX+"px;top:"+e.clientY+"px;background:#333;border:1px solid #666;border-radius:4px;z-index:1000;box-shadow:2px 2px 8px rgba(0,0,0,0.5)";

			function createOption(text, color, handler) {
				var opt = cE("div");
				opt.textContent = text;
				opt.style.cssText = "padding:8px 12px;cursor:pointer;color:" + color;
				opt.onmouseover = function() { this.style.background = "#555"; };
				opt.onmouseout = function() { this.style.background = ""; };
				opt.onclick = function() { handler(); cleanup(); };
				return opt;
			}

			menu.appendChild(createOption("Download", "#fff", function(){ downloadFile(path); }));
			menu.appendChild(createOption("Delete", "#f66", function(){ deleteFile(path); }));
			d.body.appendChild(menu);

			function cleanup() {
				if (menu.parentNode) menu.remove();
				d.onclick = null;
				menuCleanup = null;
			}

			menuCleanup = cleanup;
			setTimeout(function() { d.onclick = cleanup; }, 100);
		};

		treeRoot.appendChild(leaf);
	}

	function addList(path,items){
		for(var i=0;i<items.length;i++){
			if (items[i].type==="file" && items[i].name !== "wsec.json") { // hide wsec.json, this is redundant on purpose (done in C code too), just in case...
				var fullPath = path === "/" ? "/" + items[i].name : path + "/" + items[i].name;
				createLeaf(fullPath, items[i].name, items[i].size);
			}
		}
		fsMem();
	}

	// add file system memory info and credits at the bottom of the tree
	async function fsMem(){
		try{
			const r=await fetch(getURL("/json/info"));
			const info=await r.json();
			var c=cE("div");
		  c.style.cssText="font-size:12px;color:#aaa;margin-top:40px";
			c.textContent="by @dedehai";
			if(info&&info.fs){
				c.textContent=`by @dedehai | Memory: ${info.fs.u} KB / ${info.fs.t} KB`;
			}
			treeRoot.appendChild(c);
		}catch(e){console.error(e);}
	}

	function getCb(p){
		return function(st,resp){
			if (st==200) {
				try {
					addList(p, JSON.parse(resp));
				} catch(e) {
					console.error("Error parsing file list:", e);
				}
			} else {
				console.error("Error loading file list:", st, resp);
			}
		};
	}

	function httpGet(p){
		req.add("GET", "/edit", { func:"list", path:p }, getCb(p));
	}

	this.refreshPath=function(p){
		treeRoot.innerHTML="";
		httpGet("/");
	};

	httpGet("/");
	return this;
}

// Pretty-print ledmapX.json: print 2D maps in aligned columns, print 1D maps as single line
function prettyLedmap(json){
	try {
		let obj = JSON.parse(json);
		if (!obj.map || !Array.isArray(obj.map)) return JSON.stringify(obj, null, 2);
		let width = obj.width || obj.map.length;
		let maxLen = Math.max(...obj.map.map(n => String(n).length)); // max length of numbers for padding

		function pad(num) {
			let s = String(num);
			while (s.length < maxLen) s = " " + s;
			return s;
		}

		let rows = [];
		for (let i = 0; i < obj.map.length; i += width) {
			rows.push("    " + obj.map.slice(i, i + width).map(pad).join(", "));
		}

		let pretty = "{\n";
		for (let k of Object.keys(obj)) {
			if (k !== "map") {
				pretty += " \"" + k + "\": " + JSON.stringify(obj[k]) + ",\n"; // print all keys first (speeds up loading)
			}
		}
		pretty += " \"map\": [\n" + rows.join(",\n") + "\n  ]\n}";
		return pretty;
	} catch (e) {
		return json;
	}
}

function createEditor(element,file){
	if (!file) file="";

	var ta = cE("textarea");
	var editorDiv = cE("div");
	editorDiv.id = "ace-editor";
	editorDiv.style.display = "none";

	gId(element).appendChild(ta);
	gId(element).appendChild(editorDiv);

	var currentFile = file;
	var aceEditor = null;
	var useAce = false;

	function updateEditorMode() {
		if (!useAce || !aceEditor) return;

		// Check filename from text field or current file
		var pathField = gId("filepath");
		var filename = (pathField && pathField.value) ? pathField.value : currentFile;
		aceEditor.session.setMode(filename && filename.toLowerCase().endsWith('.json') ? "ace/mode/json" : "ace/mode/text");
	}

	// Try to initialize Ace editor if available
	function initAce(){
		if (useAce || typeof ace === 'undefined') return;
		try {
			aceEditor = ace.edit(editorDiv);
			aceEditor.setTheme("ace/theme/monokai");
			aceEditor.session.setMode("ace/mode/text");
			aceEditor.setOptions({ fontSize:"13px", fontFamily:"monospace", showPrintMargin:false, wrap:true });
			useAce = true;
			//console.log("Use Ace editor");
			switchToAce();
			updateEditorMode();

			// Monitor filename input for JSON highlighting (prevent duplicate listeners)
			var pathField = gId("filepath");
			if (pathField && !pathField.jsonListener) {
				pathField.oninput = updateEditorMode;
				pathField.jsonListener = true;
			}
		} catch(e) {
			//console.log("Ace load failed:", e);
			useAce = false;
		}
	}
	// Try now and on window load as a fallback
	setTimeout(initAce, 100);
	window.addEventListener('load', initAce);

	function switchToAce() {
		if (useAce && aceEditor) {
			ta.style.display = "none";
			editorDiv.style.display = "block";
			editorDiv.style.flex = "1";
			aceEditor.setValue(ta.value, -1);
			aceEditor.resize();
		}
	}

	function getContent() {
		return (useAce && aceEditor && editorDiv.style.display !== "none") ? aceEditor.getValue() : ta.value;
	}

	function setContent(content) {
		ta.value = content;
		if (useAce && aceEditor) aceEditor.setValue(content, -1);
	}

	// Live JSON validation for textarea
	ta.oninput = function() {
		var pathField = gId("filepath");
		var filename = pathField ? pathField.value : currentFile;
		var border = "2px solid #333";

		if (filename && filename.toLowerCase().endsWith('.json')) {
			try {
				JSON.parse(ta.value);
			} catch(e) {
				border = "2px solid #f00";
			}
		}
		ta.style.border = border;
	};

	function saveFile(filename,data){
		var finalData = data;
		// Minify JSON files before upload
		if (filename.toLowerCase().endsWith('.json')) {
			try {
				finalData = JSON.stringify(JSON.parse(data));
			} catch(e) {
				alert("Invalid JSON! Please fix syntax.");
				return;
			}
		}
		var fd=new FormData();
		fd.append("file",new Blob([finalData],{type:"text/plain"}),filename);
		req.add("POST","/upload",fd,function(st,resp){
			if (st!=200) alert("ERROR "+st+": "+resp);
			else {
				showToast("File saved");
				refreshTree();
			}
		});
	}

	function loadFile(filename){
		if (!filename) return;
		req.add("GET", "/edit", { func:"edit", path:filename }, function(st, resp) {
			gId("preview").style.display="none";
			gId("editor").style.display="flex";
			if (st==200) {
				if (filename.toLowerCase().endsWith('.json')) {
					try {
						setContent(filename.toLowerCase().includes('ledmap') ? prettyLedmap(resp) : JSON.stringify(JSON.parse(resp), null, 2));
					} catch(e) {
						setContent(resp);
					}
				} else {
					setContent(resp);
				}
			} else {
				setContent("");
			}
			currentFile = filename;
			updateEditorMode();
		});
	}

	if (file) loadFile(file);

	return {
		save:function(){
			var pathField = gId("filepath");
			var fn = pathField ? pathField.value : "";
			if (!fn) {
				alert("Please enter a filename!");
				return;
			}
			if (!fn.startsWith("/")) fn = "/" + fn;
			currentFile = fn; // Update current file
			saveFile(fn, getContent());
			loadFile(fn);
		},
		loadText:function(fn){
			currentFile=fn;
			var pathField = gId("filepath");
			if (pathField && fn) {
				pathField.value = fn.startsWith("/") ? fn.substring(1) : fn;
			}
			loadFile(fn);
		},
		clearEditor:function(){
			gId("preview").style.display="none";
			gId("editor").style.display="flex";
			// Update filename in text field
			setContent("");
			var pathField = gId("filepath");
			pathField.value = "";
			pathField.placeholder = "Filename to save";
			updateEditorMode();
		}
	};
}

function onBodyLoad(){
	var vars={};
	window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi,function(m,k,v){
		vars[decodeURIComponent(k)]=decodeURIComponent(v);
	});

	var editor=createEditor("editor",vars.file);
	globalTree=createTree("tree",editor);
	createTop("top",editor);
	// Add Ctrl+S / Cmd+S override to save the file
	document.addEventListener('keydown', function(e) {
		if ((e.ctrlKey || e.metaKey) && e.key === 's') {
			e.preventDefault();
			editor.save();
		}
	});
}
</script>
</head>
<body onload="onBodyLoad()">
	<div id="toast"></div>
	<div id="loader"><div class="loader"></div></div>
	<div id="top"></div>
	<div style="flex:1;position:relative">
		<div id="tree">
		</div>
		<div id="editor"></div>
		<div id="preview"></div>
	</div>
</body>
</html>